//
// https://github.com/ServiceStack/ServiceStack.Text
// ServiceStack.Text: .NET C# POCO JSON, JSV and CSV Text Serializers.
//
// Authors:
//   Demis Bellot (demis.bellot@gmail.com)
//
// Copyright 2012 ServiceStack Ltd.
//
// Licensed under the same terms of ServiceStack: new BSD license.
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading;
using ServiceStack.Text.Json;

namespace ServiceStack.Text.Common
{
	internal delegate void WriteMapDelegate(
		TextWriter writer,
		object oMap,
		WriteObjectDelegate writeKeyFn,
		WriteObjectDelegate writeValueFn);

	internal static class WriteDictionary<TSerializer>
		where TSerializer : ITypeSerializer
	{
		private static readonly ITypeSerializer Serializer = JsWriter.GetTypeSerializer<TSerializer>();

		internal class MapKey
		{
			internal Type KeyType;
			internal Type ValueType;

			public MapKey(Type keyType, Type valueType)
			{
				KeyType = keyType;
				ValueType = valueType;
			}

			public bool Equals(MapKey other)
			{
				if (ReferenceEquals(null, other)) return false;
				if (ReferenceEquals(this, other)) return true;
				return Equals(other.KeyType, KeyType) && Equals(other.ValueType, ValueType);
			}

			public override bool Equals(object obj)
			{
				if (ReferenceEquals(null, obj)) return false;
				if (ReferenceEquals(this, obj)) return true;
				if (obj.GetType() != typeof(MapKey)) return false;
				return Equals((MapKey)obj);
			}

			public override int GetHashCode()
			{
				unchecked
				{
					return ((KeyType != null ? KeyType.GetHashCode() : 0) * 397) ^ (ValueType != null ? ValueType.GetHashCode() : 0);
				}
			}
		}

		static Dictionary<MapKey, WriteMapDelegate> CacheFns = new Dictionary<MapKey, WriteMapDelegate>();

		public static Action<TextWriter, object, WriteObjectDelegate, WriteObjectDelegate>
			GetWriteGenericDictionary(Type keyType, Type valueType)
		{
			WriteMapDelegate writeFn;
            var mapKey = new MapKey(keyType, valueType);
            if (CacheFns.TryGetValue(mapKey, out writeFn)) return writeFn.Invoke;

            var genericType = typeof(ToStringDictionaryMethods<,,>).MakeGenericType(keyType, valueType, typeof(TSerializer));
            var mi = genericType.GetMethod("WriteIDictionary", BindingFlags.Static | BindingFlags.Public);
            writeFn = (WriteMapDelegate)Delegate.CreateDelegate(typeof(WriteMapDelegate), mi);

            Dictionary<MapKey, WriteMapDelegate> snapshot, newCache;
            do
            {
                snapshot = CacheFns;
                newCache = new Dictionary<MapKey, WriteMapDelegate>(CacheFns);
                newCache[mapKey] = writeFn;

            } while (!ReferenceEquals(
                Interlocked.CompareExchange(ref CacheFns, newCache, snapshot), snapshot));
            
            return writeFn.Invoke;
		}

		public static void WriteIDictionary(TextWriter writer, object oMap)
		{
			WriteObjectDelegate writeKeyFn = null;
			WriteObjectDelegate writeValueFn = null;

			writer.Write(JsWriter.MapStartChar);
			var encodeMapKey = false;

			var map = (IDictionary)oMap;
			var ranOnce = false;
			foreach (var key in map.Keys)
			{
				var dictionaryValue = map[key];

                var isNull = (dictionaryValue == null);
                if (isNull && !Serializer.IncludeNullValues) continue;

				if (writeKeyFn == null)
				{
					var keyType = key.GetType();
					writeKeyFn = Serializer.GetWriteFn(keyType);
					encodeMapKey = Serializer.GetTypeInfo(keyType).EncodeMapKey;
				}

				if (writeValueFn == null)
					writeValueFn = Serializer.GetWriteFn(dictionaryValue.GetType());

				JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce);

				JsState.WritingKeyCount++;
				JsState.IsWritingValue = false;

				if (encodeMapKey)
				{
					JsState.IsWritingValue = true; //prevent ""null""
					writer.Write(JsWriter.QuoteChar);
					writeKeyFn(writer, key);
					writer.Write(JsWriter.QuoteChar);
				}
				else
				{
					writeKeyFn(writer, key);
				}

				JsState.WritingKeyCount--;

				writer.Write(JsWriter.MapKeySeperator);

                if (isNull)
                {
                    writer.Write(JsonUtils.Null);
                }
                else
                {
                    JsState.IsWritingValue = true;
                    writeValueFn(writer, dictionaryValue);
                    JsState.IsWritingValue = false;
                }
			}

			writer.Write(JsWriter.MapEndChar);
		}
	}

	internal static class ToStringDictionaryMethods<TKey, TValue, TSerializer>
		where TSerializer : ITypeSerializer
	{
		private static readonly ITypeSerializer Serializer = JsWriter.GetTypeSerializer<TSerializer>();

		public static void WriteIDictionary(
			TextWriter writer,
			object oMap,
			WriteObjectDelegate writeKeyFn,
			WriteObjectDelegate writeValueFn)
		{
			if (writer == null) return; //AOT
			WriteGenericIDictionary(writer, (IDictionary<TKey, TValue>)oMap, writeKeyFn, writeValueFn);
		}

		public static void WriteGenericIDictionary(
			TextWriter writer,
			IDictionary<TKey, TValue> map,
			WriteObjectDelegate writeKeyFn,
			WriteObjectDelegate writeValueFn)
		{
		    if (map == null)
		    {
		        writer.Write(JsonUtils.Null);
                return;
		    }
			writer.Write(JsWriter.MapStartChar);

			var encodeMapKey = Serializer.GetTypeInfo(typeof(TKey)).EncodeMapKey;

			var ranOnce = false;
			foreach (var kvp in map)
			{
                var isNull = (kvp.Value == null);
                if (isNull && !Serializer.IncludeNullValues) continue;

				JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce);

				JsState.WritingKeyCount++;
                JsState.IsWritingValue = false;

				if (encodeMapKey)
				{
					JsState.IsWritingValue = true; //prevent ""null""
					writer.Write(JsWriter.QuoteChar);
					writeKeyFn(writer, kvp.Key);
					writer.Write(JsWriter.QuoteChar);
				}
				else
				{
					writeKeyFn(writer, kvp.Key);
				}
				
				JsState.WritingKeyCount--;

				writer.Write(JsWriter.MapKeySeperator);

                if (isNull)
                {
                    writer.Write(JsonUtils.Null);
                }
                else
                {
                    JsState.IsWritingValue = true;
                    writeValueFn(writer, kvp.Value);
                    JsState.IsWritingValue = false;
                }
			}

			writer.Write(JsWriter.MapEndChar);
		}
	}
}